//-----------------------------------------------------------------------
// <copyright file="ProblemImplementationBase.cs" company="Numbrella foundation">
// Copyright (c) Numbrella foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace Numbrella.Core
{
	using System;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.Globalization;
	using System.Linq;
	using System.ServiceModel;
	using Numbrella.Interaction;

	/// <summary>
	/// Represents base class for all the implementations of <see cref="IProblem"/> interface.
	/// </summary>
	/// <typeparam name="TTask">Type of <see cref="ITask"/> implementation used in the problem.</typeparam>
	/// <typeparam name="TResultContents">Type parameter for the associated <see cref="TaskResult{TResultContents}"/> implementation.</typeparam>
	[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
	public abstract class ProblemImplementationBase<TTask, TResultContents> : ProblemBase
		where TTask : TaskBase<TResultContents>
	{
		#region Fields

		/// <summary>
		/// Contains synchronization root for all the operations in this class.
		/// </summary>
		private readonly object syncRoot = new object();

		/// <summary>
		/// Contains information about all the tasks of the current problem.
		/// </summary>
		private readonly TaskDescription<TResultContents>[] taskInfo;

		/// <summary>
		/// Contains a value indicating how much times each task should be evaluated for validation.
		/// </summary>
		private readonly int taskEvaluationCount;

		/// <summary>
		/// Contains total count of the tasks of the current problem.
		/// </summary>
		private readonly int taskCount;

		/// <summary>
		/// Contains count of the completed tasks of the current problem.
		/// </summary>
		private int completedTaskCount;

		/// <summary>
		/// Contains value that indicates if current problem was solved.
		/// </summary>
		private bool isCompleted;

		#endregion

		#region Initialization

		/// <summary>
		/// Initializes a new instance of the <see cref="ProblemImplementationBase{TTask,TResultContents}"/> class.
		/// </summary>
		/// <param name="name">Unique problem name.</param>
		/// <param name="descriptionText">Problem description text.</param>
		/// <param name="taskCount">Total count of tasks of the problem.</param>
		protected ProblemImplementationBase(string name, string descriptionText, int taskCount)
			: this(name, descriptionText, taskCount, 1)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="ProblemImplementationBase{TTask,TResultContents}"/> class.
		/// </summary>
		/// <param name="name">Unique problem name.</param>
		/// <param name="descriptionText">Problem description text.</param>
		/// <param name="taskCount">Total count of tasks of the problem.</param>
		/// <param name="taskEvaluationCount">Value indicating how much times each task should be evaluated for validation.</param>
		protected ProblemImplementationBase(string name, string descriptionText, int taskCount, int taskEvaluationCount)
			: base(name, descriptionText, typeof(TTask), typeof(TaskResult<TResultContents>))
		{
			if (taskCount <= 0)
				throw new ArgumentOutOfRangeException("taskCount");
			if (taskEvaluationCount < 1)
				throw new ArgumentOutOfRangeException("taskEvaluationCount");

			this.taskCount = taskCount;
			this.taskEvaluationCount = taskEvaluationCount;
			this.taskInfo = new TaskDescription<TResultContents>[taskCount];

			for (int i = 0; i < taskCount; ++i)
				this.taskInfo[i] = new TaskDescription<TResultContents>(i, this.taskEvaluationCount);
		}

		#endregion

		#region Events

		/// <summary>
		/// Raises immediately after current problem problem has been solved.
		/// </summary>
		public override event EventHandler<EventArgs> ProblemSolved;

		/// <summary>
		/// Raises immediately after task of the current problem has been solved.
		/// </summary>
		public override event EventHandler<TaskSolvedEventArgs> TaskSolved;

		#endregion

		#region Properties

		/// <summary>
		/// Gets a value indicating whether current problem was solved (all the tasks were solved).
		/// </summary>
		public override bool IsCompleted
		{
			get { return this.isCompleted; }
		}

		/// <summary>
		/// Gets total count of the tasks of the current problem.
		/// </summary>
		public override int TaskCount
		{
			get { return this.taskCount; }
		}

		/// <summary>
		/// Gets total count of the completed tasks of the current problem.
		/// </summary>
		public override int CompletedTaskCount
		{
			get { return this.completedTaskCount; }
		}

		/// <summary>
		/// Gets value indicating how much times task result should be generated to be considered as valid.
		/// So, if <see cref="TaskEvaluationCount"/> equals to 1 then first generated result is considered as valid.
		/// But if/ for example, <see cref="TaskEvaluationCount"/> equals to 3 then result is considered as valid only when
		/// 3 different task leechers generate the same result.
		/// </summary>
		/// <remarks>
		/// <para>Defaults to 1.</para>
		/// <para><see cref="object.Equals(object)"/> method is used to check if two results are equal.</para>
		/// </remarks>
		public int TaskEvaluationCount
		{
			get { return this.taskEvaluationCount; }
		}

		/// <summary>
		/// Gets validator for task results.
		/// Override this property to provide your own validator.
		/// </summary>
		/// <remarks>
		/// Defaults to null which means no validation at all.
		/// </remarks>
		public virtual IResultValidator<TResultContents> ResultValidator
		{
			get { return null; }
		}

		#endregion

		#region Methods

		/// <summary>
		/// Gets if problem has tasks that were not solved yet.
		/// </summary>
		/// <returns>True, if problem has some tasks to solve; otherwise, false.</returns>
		public override bool GetIfHasTasksToSolve()
		{
			lock (this.syncRoot)
			{
				for (int i = 0; i < this.taskCount; ++i)
				{
					if (this.taskInfo[i].GetIfMoreSolversAreRequired(this.GetTimeoutForTask(i)))
						return true;
				}

				return false;
			}
		}

		/// <summary>
		/// Retrieves task that is pending for execution.
		/// </summary>
		/// <param name="userName">Name of the user who wants to take a task.</param>
		/// <returns>Pending task.</returns>
		public override ITask GetPendingTask(string userName)
		{
			lock (this.syncRoot)
			{
				// Find task id
				int id = this.FindIdOfTaskToSolve(userName);

				// Create new instance of task or return null if we found no task
				return id == -1 ? null : this.TakeTask(id, userName);
			}
		}

		/// <summary>
		/// Submits result of solved task.
		/// </summary>
		/// <param name="result">Task result.</param>
		/// <param name="userName">Name of the user who wants to submit result.</param>
		public override void SubmitResult(ITaskResult result, string userName)
		{
			if (result == null)
				ExceptionBuilder.ThrowArgumentNullException("result");
			if (String.IsNullOrEmpty(userName))
				ExceptionBuilder.ThrowArgumentNullException("userName");

			// Check result type
			TaskResult<TResultContents> taskResult = result as TaskResult<TResultContents>;
			if (taskResult == null)
			{
				string message = string.Format(
					CultureInfo.CurrentCulture,
					"Invalid result type given (should be {0}).",
					typeof(TaskResult<TResultContents>).Name);
				ExceptionBuilder.ThrowArgumentException("result", message);
			}

			TaskDescription<TResultContents> taskInformation = this.taskInfo[taskResult.TaskId];

			// Prior validation (check result through custom validator)
			bool resultCorrect = this.ValidatePrior(taskResult);
			if (!resultCorrect)
			{
				ExceptionBuilder.ThrowInvalidOperationException(string.Format(
					CultureInfo.CurrentCulture,
					"Result for task {0} from user {1} did not pass prior validation.",
					taskResult.TaskId,
					userName));
			}

			lock (this.syncRoot)
			{
				// Check if task was taken by someone and not yet returned
				if (!taskInformation.IsWaitingForResultFromUser(userName))
				{
					ExceptionBuilder.ThrowInvalidOperationException(string.Format(
						CultureInfo.CurrentCulture,
						"Task with ID {0} is not waiting for result from user {1}.",
						taskResult.TaskId,
						userName));
				}

				taskInformation.SubmitResult(taskResult, userName);
				if (taskInformation.Status != TaskStatus.Completed)
				{
					Trace.TraceInformation(
						"Result for task {0} does not pass posterior validation yet (probably {1} more results required).",
						taskResult.TaskId,
						taskInformation.RequiredResultsApproximateAmount);
					return;
				}

				Trace.TraceInformation("Result for task {0} successfully passes all the necessary validation.", taskResult.TaskId);

				this.completedTaskCount += 1;
				if (this.completedTaskCount == this.taskCount)
					this.isCompleted = true;
			}

			//// TODO: Probably this user function should not be forced to act as thread-safe. 
			this.AccumulateResult(taskResult);

			// Raise TaskSolved event
			IEnumerable<string> userNames = from solveAttempt in taskInformation.ValidSolveAttempts select solveAttempt.UserName;
			this.RaiseTaskSolved(new TaskSolvedEventArgs(taskResult.TaskId, userNames));

			// Raise ProblemSolved event
			if (this.isCompleted)
				this.RaiseProblemSolved(EventArgs.Empty);
		}

		/// <summary>
		/// Implement this method to create task for given identifier.
		/// </summary>
		/// <param name="id">Unique task identifier.</param>
		/// <returns>Created task.</returns>
		protected abstract TTask CreateTaskForId(int id);

		/// <summary>
		/// Implement this method to accumulate results from solved tasks.
		/// </summary>
		/// <remarks>
		/// Implementation of this function should be thread-safe. That is because notification is raised
		/// immediately after task result was submitted and so more than one task solver can report
		/// results simultaneously.
		/// </remarks>
		/// <param name="result">Result of the task that has been just solved.</param>
		protected abstract void AccumulateResult(TaskResult<TResultContents> result);

		/// <summary>
		/// This method is called when <see cref="TaskSolved"/> event is raised.
		/// </summary>
		/// <param name="e"><see cref="TaskSolved"/> event arguments.</param>
		protected virtual void OnTaskSolved(TaskSolvedEventArgs e)
		{
		}

		/// <summary>
		/// This method is called when <see cref="ProblemSolved"/> event is raised.
		/// </summary>
		/// <param name="e"><see cref="ProblemSolved"/> event arguments.</param>
		protected virtual void OnProblemSolved(EventArgs e)
		{
		}

		/// <summary>
		/// Override this method to specify task solve timeout for task with given identifier.
		/// </summary>
		/// <remarks>
		/// Default timeout is 24 hours.
		/// </remarks>
		/// <param name="taskId">Unique task identifier.</param>
		/// <returns>Task solve timeout for task with given identifier.</returns>
		protected virtual TimeSpan GetTimeoutForTask(int taskId)
		{
			return TimeSpan.FromHours(24);
		}

		/// <summary>
		/// Retrieves task with given identifier and updates task status information.
		/// </summary>
		/// <param name="taskId">Unique task identifier.</param>
		/// <param name="userName">User name.</param>
		/// <returns>Task instance.</returns>
		private ITask TakeTask(int taskId, string userName)
		{
			Debug.Assert(taskId >= 0 && taskId < this.taskCount, "Task identifier should have valid value.");
			Debug.Assert(!String.IsNullOrEmpty(userName), "User name can not be empty");

			this.taskInfo[taskId].TakeTask(userName);
			return this.CreateTaskForId(taskId);
		}

		/// <summary>
		/// Finds pending task for user with given name.
		/// </summary>
		/// <param name="userName">User name.</param>
		/// <returns>Id of pending task if it was found; otherwise, -1.</returns>
		private int FindIdOfTaskToSolve(string userName)
		{
			// Look for pending task first
			for (int i = 0; i < this.taskCount; ++i)
			{
				if (this.taskInfo[i].GetIfMoreSolversAreRequired() && this.taskInfo[i].CanUserTakeTask(userName))
					return i;
			}

			// Removes solve attempts with timeout exceeded and tries once again
			for (int i = 0; i < this.taskCount; ++i)
			{
				if (this.taskInfo[i].Status != TaskStatus.Processing)
					continue;

				this.taskInfo[i].UpdateTimedOutSolveAttempts(this.GetTimeoutForTask(i));
				if (this.taskInfo[i].GetIfMoreSolversAreRequired() && this.taskInfo[i].CanUserTakeTask(userName))
					return i;
			}

			// No pending task found
			return -1;
		}

		/// <summary>
		/// Raises <see cref="TaskSolved"/> event.
		/// </summary>
		/// <param name="e"><see cref="TaskSolved"/> event arguments.</param>
		private void RaiseTaskSolved(TaskSolvedEventArgs e)
		{
			EventHandler<TaskSolvedEventArgs> taskSolvedHandler = this.TaskSolved;
			if (taskSolvedHandler != null)
				taskSolvedHandler(this, e);

			this.OnTaskSolved(e);
		}

		/// <summary>
		/// Raises <see cref="ProblemSolved"/> event.
		/// </summary>
		/// <param name="e"><see cref="ProblemSolved"/> event arguments.</param>
		private void RaiseProblemSolved(EventArgs e)
		{
			EventHandler<EventArgs> problemSolvedHandler = this.ProblemSolved;
			if (problemSolvedHandler != null)
				problemSolvedHandler(this, e);

			this.OnProblemSolved(e);
		}

		/// <summary>
		/// Validates result using explicit validator.
		/// </summary>
		/// <param name="taskResult">Task result to validate.</param>
		/// <returns>True if result passes validation; otherwise, false.</returns>
		private bool ValidatePrior(TaskResult<TResultContents> taskResult)
		{
			Debug.Assert(taskResult != null, "Task result can not be null.");

			// Validate results through custom validator (if available)
			IResultValidator<TResultContents> validator = this.ResultValidator;
			//// TODO: Probably validator should not be forced to act as thread-safe.
			return validator == null || validator.IsCorrect(taskResult);
		}

		#endregion
	}
}